// vtf webgl benchmark.

// Intent is to measure vertex-texture-fetch speed.

function vtf() {
  var me = this;
  me.seq_ = 0;
  me.tick_closure_ = function() {
    me.tick();
  };

  me.tri_count_ = 0;
  me.draw_count_ = 0;

  // Put the camera in a standard place.
  reset_view(
      {x: 2.3, y: 0, z: 0},  // pos
      {x: -1, y: 0, z: 0},  // dir
      {x: 0, y: 1, z: 0} // up
             );

  me.program_ = fetch_load_and_link_program('static/vtf.vs',
                                            'static/vtf.fs');
  if (!me.program_) {
    log('vtf: couldn\'t load program');
    return;
  }
  me.mvp_location_ = gl.getUniformLocation(me.program_, 'MVP');
  me.position_location_ = gl.getAttribLocation(me.program_, 'position');
  me.t0_uv_location_ = gl.getAttribLocation(me.program_, 't0_uv');
}

vtf.prototype.tick = function() {
  var me = this;

  if (!me.program_) {
    // Bail out, but log a metric.
    add_metric('vtf/load_program_failed', 1);
    return;
  }

  var tests_params = [
      {layers: 1, dim: 128, prim: 'strip'},
                         ];

  if (me.seq_ >= tests_params.length * 2) {
    // Done.
    me.shutdown_world();
    return;
  }

  // Do one set of parameters.
  var params = tests_params[me.seq_ >> 1];

  if ((me.seq_ & 1) == 0) {
    // Initialize.
    me.init_world(params.layers, params.dim, params.prim);

    me.draw();

    // Yield back to the browser so it shows the scene we are testing.
    me.seq_++;
    continue_benchmark(me.tick_closure_);
    return;
  }

  // Warm up.
  for (var i = 0; i < 10; i++) {
    me.draw();
  }

  // Do a bunch of draws -- run for at least two seconds.
  me.tri_count_ = 0;
  me.draw_count_ = 0;
  var start = (new Date()).getTime();

  for (;;) {
    for (var i = 0; i < 10; i++) {
      me.draw();
    }
    var now = (new Date()).getTime();
    if (now - start > 2000) {
      break;
    }
  }

  var end = (new Date()).getTime();
  var dt = end - start;

  // Compute rates.
  var tps = Math.round(1000 * me.tri_count_ / dt);
  var dps = Math.round(1000 * me.draw_count_ / dt);

  var namebase = "vtf/l" + params.layers + ".dim" + params.dim + "." + params.prim;
  add_metric(namebase + ".tps", tps);
  add_metric(namebase + ".dps", dps);
  me.seq_++;
  continue_benchmark(me.tick_closure_);
};

vtf.prototype.init_world = function(depth_x, dim, prim_type) {
  var me = this;
  check_gl_error('vtf_init_world pre');
  me.world_ = [];
  for (var d = 0; d < depth_x; d++) {
    for (var j = 0; j < 5; j++) {
      for (var i = 0; i < 5; i++) {
        var model = {};
        me.init_model(model, prim_type, i, j, d, dim);
        me.world_.push(model);
      }
    }
  }
  check_gl_error('vtf_init_world post');
};

vtf.prototype.make_grid_triangles = function(dim) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  //
  // TODO: Investigate alternative vert orderings and slab heights.
  var SLAB_HEIGHT = 8;
  var inds = new Uint16Array((dim - 1) * (dim - 1) * 6);
  var next_i = 0;
  for (var slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (var i = 0; i < dim - 1; i++) {
      var end_j = Math.min(dim - 1, slab_j + SLAB_HEIGHT);
      for (var j = slab_j; j < end_j; j++) {
        var vi = j * dim + i;
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        inds[next_i++] = vi;
        inds[next_i++] = vi + dim + 1;
        inds[next_i++] = vi + dim;
        inds[next_i++] = vi;
        inds[next_i++] = vi + 1;
        inds[next_i++] = vi + dim + 1;
      }
    }
  }
  return inds;
};

vtf.prototype.make_grid_triangle_strip = function(dim) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  //
  // TODO: Investigate alternative vert orderings and slab heights.
  var SLAB_HEIGHT = 8;
  var inds = [];
  for (var slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (var i = 0; i < dim - 1; i++) {
      var end_j = Math.min(dim - 1, slab_j + SLAB_HEIGHT);
      var vi = slab_j * dim + i;
      if (inds.length) {
        // Start the strip with a degenerate if we're connecting to a
        // previous strip.
        inds.push(vi + 1);
      }
      inds.push(vi + 1);
      inds.push(vi);
      for (var j = slab_j; j < end_j; j++) {
        //  *--*
        //  |\ | ...
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //   .
        //   .
        //   .
        inds.push(vi + dim + 1);
        inds.push(vi + dim);
        vi += dim;
      }
      // End the strip with a degenerate tri in order to start the
      // next strip.
      inds.push(inds[inds.length - 1]);
    }
  }
  var inds_array = new Uint16Array(inds);
  return inds_array;
};

vtf.prototype.init_model = function(model, prim_type, x, y, d, grid_dim) {
  check_gl_error('init_model pre');

  var origin = {x: d * -0.15,
                y: (x - 2) * 0.5,
                z: (y - 2) * 0.5};
  var up = {x: 1, y: 0, z: 0}; // vectorNormalize(origin);
  var dir;
  if (Math.abs(up.y) > 0.8) {
    dir = {x: 0, y: 0, z: 1};
  } else {
    dir = {x: 0, y: 1, z: 0};
  }
  var right = crossProduct(dir, up);
  dir = vectorNormalize(crossProduct(up, right));

  // Pick some randomish constants for modulating the model.
  var seed = x + y * 27 + d * 129;
  var c0 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 13 + 1) % 8];
  var c1 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 23 + 2) % 8];
  var c2 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 7 + 3) % 8];
  var c3 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 17 + 4) % 8];
  var c4 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 5 + 5) % 8];
  var c5 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 3 + 6) % 8];
  var c6 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 11 + 7) % 8];
  var c7 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 31 + 0) % 8];
  var c8 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 43 + 1) % 8];
  var c9 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 119 + 7) % 8];
  var c10 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8][(seed * 67 + 2) % 8];

  var DIM = grid_dim;
  var verts = new Float32Array(DIM * DIM * 5);
  for (var j = 0; j < DIM; j++) {
    for (var i = 0; i < DIM; i++) {
      // Position.
      var u = i / (DIM - 1);
      var v = j / (DIM - 1);
      verts[(j * DIM + i) * 5 + 0] = (u - 0.5) * 0.5;
      verts[(j * DIM + i) * 5 + 1] = 0; // (Math.sin(u * c0 * 10 + c1) + Math.cos(v * c2 * 20) + c3) * c4 * 0.15;
      verts[(j * DIM + i) * 5 + 2] = (v - 0.5) * 0.5;

      // Texture UV.
      verts[(j * DIM + i) * 5 + 3] = u;
      verts[(j * DIM + i) * 5 + 4] = v;
    }
  }
  var verts_buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, verts_buf);
  gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);

  check_gl_error('init_model 1');

  var inds_array;
  var index_type;
  if (prim_type == "tris") {
    inds_array = this.make_grid_triangles(DIM);
    index_type = gl.TRIANGLES;
  } else {
    inds_array = this.make_grid_triangle_strip(DIM);
    index_type = gl.TRIANGLE_STRIP;
  }
  var inds_buf = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, inds_buf);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, inds_array, gl.STATIC_DRAW);
  var index_count = inds_array.length;

  check_gl_error('init_model 2');

  // Generate a texture.
  var TEX_DIM = 128;
  var imdata = new Uint8Array(TEX_DIM * TEX_DIM * 4);
  for (var j = 0; j < TEX_DIM; j++) {
    for (var i = 0; i < TEX_DIM; i++) {
      var u = i / (TEX_DIM - 1);
      var v = j / (TEX_DIM - 1);
      var vi = (j * TEX_DIM + i) * 4;
      imdata[vi + 0] = Math.abs(Math.sin((u * c5 + c8) * 12)) * 255;    // red
      imdata[vi + 1] = Math.abs(Math.cos((v * c6 + c9) * 16)) * 255;   // green
      imdata[vi + 2] = Math.cos(((u + c7) * (v + c8) + c10) * 100) * 127 + 128;   // blue
      imdata[vi + 3] = 255;   // alpha
    }
  }
  check_gl_error('init_model 2');
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, TEX_DIM, TEX_DIM, 0, gl.RGBA, gl.UNSIGNED_BYTE, imdata);
  gl.generateMipmap(gl.TEXTURE_2D);
  gl.bindTexture(gl.TEXTURE_2D, null);

  check_gl_error('init_model 3');

  var model_mat = makeOrientationAffine(origin, dir, up);
  var model_mat_gl = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
  affineMatrixToGlMatrix(model_mat, model_mat_gl);

  model.matrix_gl = model_mat_gl;
  model.vertices = verts_buf;
  model.indices = inds_buf;
  model.index_count = index_count;
  model.index_type = index_type;
  // gl.LINES -- poor-person's wireframe
  model.triangle_count = (DIM - 1) * (DIM - 1) * 2;
  model.texture = texture;

  check_gl_error('init_model post');
};

vtf.prototype.shutdown_model = function(model) {
  // Release the pieces.
  gl.deleteTexture(model.texture);
  gl.deleteBuffer(model.vertices);
  gl.deleteBuffer(model.indices);
  check_gl_error('shutdown_model');
};

vtf.prototype.shutdown_world = function() {
  for (var i = 0; i < this.world_.length; i++) {
    this.shutdown_model(this.world_[i]);
  }
  this.world_ = [];
};

vtf.prototype.draw_model = function(model) {
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, model.texture);

  gl.bindBuffer(gl.ARRAY_BUFFER, model.vertices);
  gl.vertexAttribPointer(this.position_location_, 3, gl.FLOAT, false, 4 * 5, 0);
  gl.vertexAttribPointer(this.t0_uv_location_, 2, gl.FLOAT, false, 4 * 5, 4 * 3);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indices);
  gl.drawElements(model.index_type, model.index_count,
                  gl.UNSIGNED_SHORT, 0);
  this.tri_count_ += model.triangle_count;
  this.draw_count_ += 1;
};

vtf.prototype.draw_world = function(world) {
  viewMatrixJs = makeViewFromOrientation(cameraOrient);
  affineMatrixToGlMatrix(viewMatrixJs, viewMatrixGl);

  var vp = myMatrixMultiply(projectionMatrix, viewMatrixGl);

  for (var i = 0; i < world.length; i++) {
    var model = world[i];
    var mvp = myMatrixMultiply(vp, model.matrix_gl);
    gl.uniformMatrix4fv(this.mvp_location_, false, mvp);
    this.draw_model(model);
  }
  check_gl_error('draw_world');
};

vtf.prototype.draw = function() {
  // TODO: cleanup accessing these globals (projectionMatrix, fovy,
  // width, height, z_near, z_far).
  projectionMatrix = myGluPerspective(fovy, width / height, z_near, z_far);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  gl.depthFunc(gl.LEQUAL);
  gl.useProgram(this.program_);

  this.draw_world(this.world_);

  check_gl_error('draw');
};
